/**
* Created with CLion.
* User:Lenovo
* Date:2023-04-03
* Time:18:43
* Description:程序环境和预处理
*/

#include <stdio.h>
#include <stdlib.h>
#define DEBUG_PRINT printf("file:%s\tline:%d\t date:%s\ttime:%s\n", \
                           __FILE__, __LINE__, \
                           __DATE__, __TIME__)
#define SQUARE(x) ((x) * (x))
#define DOUBLE(x) ((x) + (x))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
#define print_format(num, format) \
                     printf("the value of "#num" is " format, num)
#define CAT(X, Y) X##Y
#define MALLOC(num, type) (type *) malloc(num * sizeof(type))
#define M 100
#define __DEBUG__

extern int Add(int, int);
extern int Max(int, int);
int Class110 = 2023;

int main()
{
    //在 ANSI C 的任何一种实现中，存在两个不同的环境
    //第一种是翻译环境，在这个环境中源代码被转换成可执行的机器指令
    //第二种是执行环境，它用于执行实际代码

    // .c 文件 ————————————————> .exe 文件 ————————> 执行
    //                |                       |
    //             编译环境                 运行环境
    //             |     |
    //  ------- 编译器  链接器
    //  |     |    |
    // 预编译 编译 汇编
    //在一个项目中，众多的源文件被编译器依次单独编译，生成目标文件 .obj，再由链接器链接生成可执行文件 .exe
    {
        int a = 0;
        int b = 0;
        scanf("%d %d", &a, &b);
        int c = Add(a, b);
        printf("%d\n", c);
    }
    //预编译过程的操作
        // 1.头文件的包含 #include
        // 2.注释的删除
        // 3.#define 定义的符号的替换
    //都是文本操作 - 预处理的指令相关

    // gcc 指令：
    // gcc -E test.c -o test.i  预处理完成之后就停下来，预处理之后产生的结果都放在 test.i 文件中
    // gcc -S test.c  编译完成之后就停下来，结果保存在 test.s 中
    // gcc -C test.c  汇编完成之后就停下来，结果保存在 test.o 中

    //编译阶段
    //把 C 语言代码翻译成了汇编代码
        // 1.语法分析
        // 2.词法分析
        // 3.语义分析
        // 4.符号汇总

    //汇编阶段
        // 1.生成 test.o 的文件 - 目标文件，二进制文件
        // 2.把汇编代码转换成二进制的指令
        // 3.形成符号表

    //链接阶段
        // 1.合并段表
        // 2.符号表的合并和重定位

    //符号的操作
        // 1.符号汇总(编译阶段)：把一些特殊的符号都标记下来，如函数名和全局变量等
        // 2.形成符号表(汇编阶段)：符号表中存在着每个符号的地址
        // 3.合并段表(链接阶段)：目标文件，已经是二进制文件了，这种目标文件是有格式的，以 gcc 编译产生的目标文件为例：目标文件的格式是 elf，每个可执行文件都被分成一段一段的，可执行程序的格式也是 elf 的，于是把相同的段合并起来，形成了可执行文件
        // 4.符号表的合并和重定位(链接阶段)：合并每个目标文件的符号表，把相同的符号地址进行筛选，去除掉无效地址，形成全新的符号表

    //符号表是用来进行跨文件操作的时候使用的
    //函数具有外部链接属性 - 符号表
    //错误：无法解析的外部符号 - 链接错误，没有在符号表中成功链接

    //预处理详解
    {
        //预定义符号 - 可以直接用的符号
        // __FILE__  进行编译的源文件
        // __LINE__  代码所在行数
        // __DATE__  文件被编译的日期
        // __TIME__  文件被编译的时间
        // __STDC__  如果编译器严格遵循 ANSI C，其值为 1，否则未定义
        {
            int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
            for (int i = 0; i < 10; i++)
            {
                printf("%d----%s, %s, %s, line = %d\n", arr[i], __FILE__, __DATE__, __TIME__, __LINE__);
            }
            printf("%d\n", __STDC__);
            printf("%s\n", __FUNCTION__);
        }

        // #define 定义标识符 - 直接替换
        // 1.用简短的名字代替关键字           #define reg register
        // 2.用形象的语言代替操作             #define do_forever for(;;)
        // 3.奇怪的替换(?)                  #define CASE break;case
        // 4.如果 #define 的后面太长，可以换行，但是在每次换行之前都要写续行符 '\'
        DEBUG_PRINT;

        // #define 的后面用加 ; 吗？
        //不用，替换的时候会把分号也替换进去，有可能出问题

        // #define 定义宏
        {
            // #define 机制包括了一个规定，允许把参数替换到文本中，这种实现通常称为宏(macro)或定义宏(define macro)
            // #define name(parameter-list) stuff
            //其中的 parameter-list 是一个由逗号隔开的符号表，他们可能出现在 stuff 中
            //注意：参数列表的左括号必须和 name 紧邻，否则参数列表就会被解释为 stuff 的一部分

            printf("%d\n", SQUARE(8));
            printf("%d\n", SQUARE(1 + 7));// 1 + 7 * 1 + 7 = 15 ?
            printf("%d\n", 10 * DOUBLE(4));// 10 * 4 + 4 = 44 ?
            //结论是变量和最外面都要加括号

            printf("%d\n", MAX(3, 5));

            // #define 替换规则
            // 1.对宏的参数进行扫描，看看是否有 #define 定义的标识符，如果有，他们首先被替换
            // 2.替换文本被直接插入原文，对于宏，参数名被他们的值所替换
            // 3.最后，再次对结果文件进行扫描，看看是否有其他 #define 定义的符号，如果有，就重复上述过程
        }

        // # 和 ##
        {
            int a = 10;
            printf("the value of a is %d\n", a);
            int b = 20;
            printf("the value of b is %d\n", b);
            float f = 3.14F;
            printf("the value of f is %f\n", f);

            //上面这个代码段有些冗余，利用函数是解决不了的

            printf("hello world\n");
            printf("hello " "world\n");
            //打印的都是 hello world，说明 C 语言中的字符串可以自动组合在一起

            print_format(a, "%d\n");
            print_format(b, "%d\n");
            print_format(f, "%f\n");
            // # 的作用：在宏体的 stuff 区实现，把一个宏参数替换成对应的字符串
        }
        {
            // ## 可以把它两边的符号合并成一个符号
            printf("%d\n", CAT(Class, 110));
            //前提是合并的符号要有意义，否则结果就是未定义的
        }

        //带有副作用的宏参数
        {
            //代码 1
            int a = 10;
            int b = a + 1;// b 得到的是 11，a 还是 10
        }
        {
            //代码 2
            int a = 10;
            int b = ++a;// b 得到的是 11，但是 a 也变了，变成了 11
        }
        //代码 2 是有副作用的 -> a = 11 - 带来了另一个我们不期望的结果
        // getchar() - 虽然给了一个字符，但是缓冲区中的字符也被读走了，下次就拿不到了
        {
            int a = 3;
            int b = 5;
            int c = MAX(a++, b++);
            printf("%d\n", c);// 6
            printf("%d\n", a);// 4
            printf("%d\n", b);// 7

            // int c = ((a++) > (b++) ? (a++) : (b++))
        }
        //宏的参数替换到宏体内不止一次，那么在使用这个宏的时候就会出现危险，导致某种不可预知的结果
        //换成函数就没有这种问题 - 函数参数分为形参和实参
        {
            int a = 3;
            int b = 5;
            int c = Max(a++, b++);
            printf("%d\n", a);// 4
            printf("%d\n", b);// 6
            printf("%d\n", c);// 5
        }
        //左值，右值
        /*
         * int a = 10;
         * a = 20;      //这里使用的是 a 的空间，我们说 a 是左值
         * int c = a;   //这里使用的是 a 中存放的数据，我们说 a 是右值
         */

        //宏和函数对比
        {
            int a = 3;
            int b = 5;
            int m1 = Max(a, b);
            int m2 = MAX(a, b);
            printf("%d %d\n", m1, m2);
        }
        //宏比函数在程序的规模和速度方面更胜一筹
        //函数调用：
        // 1.函数调用前准备(传参，函数栈帧空间的维护)
        // 2.主要运算
        // 3.函数返回(返回值的处理，函数栈帧的销毁)
        //宏没有调用函数的准备和返回的开销

        //函数的参数必须声明为特定的类型，而宏是类型无关的(没有类型检查) - 更灵活

        //宏的缺点：
        // 1.每次调用宏的时候都会把宏体替换到程序中，可能大幅度增加代码的长度
        // 2.宏是没法调试的 - 预编译阶段已经完成了替换
        // 3.宏由于类型无关，也就不够严谨
        // 4.宏可能会带来运算符优先级的问题，导致程序运算出错
        // 5.宏不能递归，但是函数可以递归

        //宏的优点：
        //宏的参数可以出现类型，但是函数做不到
        {
            //原来的写法
            int *p = (int *) malloc(10 * sizeof(int));
            if (p == NULL)
            {
                return 1;
            }

            //宏的写法
            int *p2 = MALLOC(10, int);
            // MALLOC(10, float);
            if (p2 == NULL)
            {
                return 1;
            }
        }
        //功能比较简单 -> 采用宏
        //功能比较复杂 -> 采用函数

        // C / C++ 引入了一个关键字：inline
        //内联函数：具有了函数的优点，也具有宏的优点

        //命名约定：函数和宏
        //宏名全部大写，函数名不要全部大写

        // #undef
        //这条指令用于取消一个宏定义
        {
            printf("%d\n", M);
#undef M
            /* printf("%d\n", M); */ // err
        }

        //命令行定义
        {
            /*
            int arr[SZ];
            for (int i = 0; i < SZ; i++)
            {
                arr[i] = i;
            }
            for (int i = 0; i < SZ; i++)
            {
                printf("%d ", arr[i]);
            }
            printf("\n");
            */
            //在命令行里输入 gcc -文件路径+文件名 -D SZ=10
            //自动替换
        }

        //条件编译 - 决定一些代码究竟执行还是不执行
        {
            int arr[10] = { 0 };
            for (int i = 0; i < 10; i++)
            {
                arr[i] = i;
#ifdef __DEBUG__ //如果定义了 __DEBUG__，下面的代码就会执行
                printf("%d ", arr[i]);
#endif
            }
            printf("\n");
        }
        //条件编译指令
        {
            // 1.
            /* #if 常量表达式
             * // ...
             * #endif
             */
            //常量表达式的值为真 - 参与编译
            {
#if 1
                printf("hehe\n");
#endif
                //不要这样写：
                int a = 2;
#if a == 2
                printf("hehe\n");
#endif
                //常量表达式 - 因为编译的时候还没有 a 这个变量
            }
            // 2.多分支的条件编译
            /* #if 常量表达式
             * // ...
             * #elif 常量表达式
             * // ...
             * #else
             * // ...
             * #endif
             */
            {
#if 1 == 2
                printf("hehe\n");
#elif 2 == 3
                printf("haha\n");
#elif 3 == 4
                printf("heihei\n");
#else
                printf("hengheng\n");
#endif
            }
            // 3.判断是否被定义
            /* #if define(symbol)
             * // ...
             * #endif
             */
            //等同于
            /* #ifdef symbol
             * // ...
             * #endif
             */
            //详见上面函数
            /* #if !defined(MAX)
             * // ...
             * #endif
             */
            //等同于
            /* #ifndef MAX
             * // ...
             * #endif
             */
            // 4.嵌套指令
#ifdef OS_UNIX
    #ifdef OPTION1

    #endif
    #ifdef OPTION2

    #endif
#elif defined (OS_MSDOS)
    #ifdef OPTION2

    #endif
#endif
        }

        //文件包含

        // #include "自己写的头文件"
        // #include <stdio.h>
        // "" - 双引号包含头文件查找方式
        //先在源文件所在的目录里面找，如果找不到就去标准位置查找头文件，如果还找不到就提示编译错误
        // <> - 尖括号包含头文件的查找方式
        //直接去标准位置查找头文件，如果找不到就提示编译错误
        //标准位置：库文件的所在位置
        //不推荐用 "" 来包含库文件，因为查找的效率低，并且不容易辨别是不是自己写的文件

        //嵌套文件包含
        //如果一个头文件被多次利用，利用它的这些文件最终又被同一个文件利用，那么就可能出现一个头文件在代码中重复出现的情况
        //解决方法：条件编译
        // 1.
        /* #ifndef __TEST_H__
         * #define __TEST_H__
         * // ...
         * #endif
         */
        // 2.
        /* #pragma once
         * // ...
         */

        //其他预处理指令
        // #error
        // #pragma once/message/pack/comment ...
        // #line
    }

    return 0;
}